package miggy.ui;

import miggy.Debugger;
import miggy.DisassembledItem;
import miggy.utils.TextUtil;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.logging.Logger;

/*
//  Miggy - Java Amiga MachineCore
//  Copyright (c) 2008, Tony Headford
//  All rights reserved.
//
//  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
//  following conditions are met:
//
//    o  Redistributions of source code must retain the above copyright notice, this list of conditions and the
//       following disclaimer.
//    o  Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
//       following disclaimer in the documentation and/or other materials provided with the distribution.
//    o  Neither the name of the Miggy Project nor the names of its contributors may be used to endorse or promote
//       products derived from this software without specific prior written permission.
//
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
//  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
//  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
//  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
//  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
//  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// $Revision: 21 $
*/
public final class DisassemblyPanel extends JPanel implements ComponentListener, AdjustmentListener, MouseWheelListener,
		KeyListener, MouseListener, ActionListener
{
	private static Logger logger = Logger.getLogger(DisassemblyPanel.class.getName());
	private final Debugger debugger;
	private final JList list;
	private final DisassemblyModel model;
	private final JScrollBar scroller;

	private final int listCellHeight;
	private final int borderWidth = 2;
	private final int baseline;
	private int listCellWidth;
	private final JTextField addrBox;
	private final JTextField cycleBox;
	private final JButton addrBoxBtn;
	private final JButton cycleBoxBtn;

	private boolean resizing;

	public DisassemblyPanel(Debugger debugger)
	{
		super(new BorderLayout());

		this.debugger = debugger;
		resizing = false;

		setMinimumSize(new Dimension(300, 200));
		Font listFont = new Font("Courier New", Font.PLAIN, 13);

		JPanel pane = new JPanel(new BorderLayout());

		JPanel gotoPane = new JPanel(new BorderLayout());
		gotoPane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
		gotoPane.add(new JLabel("Goto: "), BorderLayout.WEST);
		addrBox = new JTextField();
		gotoPane.add(addrBox, BorderLayout.CENTER);
		addrBoxBtn = new JButton("Go");
		addrBoxBtn.addActionListener(this);
		gotoPane.add(addrBoxBtn, BorderLayout.EAST);
		pane.add(gotoPane, BorderLayout.CENTER);

		JPanel cycles = new JPanel(new BorderLayout());
		cycles.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
		cycles.add(new JLabel("Cycles: "), BorderLayout.WEST);
		cycleBox = new JTextField();
		cycleBox.setEditable(false);
		cycleBox.setPreferredSize(new Dimension(100, 20));
		cycles.add(cycleBox, BorderLayout.CENTER);
		cycleBoxBtn = new JButton("Clear");
		cycles.add(cycleBoxBtn, BorderLayout.EAST);
		pane.add(cycles, BorderLayout.EAST);

		add(pane, BorderLayout.NORTH);

		model = new DisassemblyModel(10);
		list = new JList(model);

		list.setFont(listFont);

		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		list.setLayoutOrientation(JList.VERTICAL);
		list.setVisibleRowCount(-1);

		FontMetrics metrics = getFontMetrics(listFont);
		baseline = metrics.getAscent() + borderWidth;
		listCellHeight = metrics.getHeight() + (2 * borderWidth);
		listCellWidth = 400;

		list.setCellRenderer(new DisassemblyRenderer(baseline, listCellHeight, listCellWidth));
		list.setPrototypeCellValue(model.getElementAt(0));
		list.setVisibleRowCount(40);
		list.setFixedCellWidth(400);

		list.addComponentListener(this);
		list.addMouseWheelListener(this);
		list.addKeyListener(this);
		add(list, BorderLayout.CENTER);

		scroller = new JScrollBar(JScrollBar.VERTICAL, 0, 10, 0, 0x7fffff);
		scroller.addAdjustmentListener(this);
		add(scroller, BorderLayout.EAST);
	}

	public final int getSelectedAddress()
	{
		//return the address of the currently selected item
		int pos = list.getSelectedIndex();
		if(pos != -1)
		{
			return ((DisassembledItem)model.getElementAt(pos)).getAddress();
		}
		return -1;
	}

	public final void updateData()
	{
		model.updateAll();
		//update the cycles
		cycleBox.setText(String.valueOf(debugger.getCycles()));
	}

	/**
	 * Invoked when the component's size changes.
	 */
	public final void componentResized(ComponentEvent e)
	{
		//we want to know how many rows are now visible in our list
		resizing = true;
		Dimension d = list.getSize();
		int numRows = (int)(((float)d.height / (float)listCellHeight) + 0.5f);
		model.setSlidingWindowSize(numRows);
		adjustBlockIncrement();
		resizing = false;
	}

	/**
	 * Invoked when the component's position changes.
	 */
	public final void componentMoved(ComponentEvent e)
	{
		//not interested
	}

	/**
	 * Invoked when the component has been made visible.
	 */
	public final void componentShown(ComponentEvent e)
	{
		//not interested
	}

	/**
	 * Invoked when the component has been made invisible.
	 */
	public final void componentHidden(ComponentEvent e)
	{
		//not interested
	}


	private void adjustBlockIncrement()
	{
		//everytime the view changes we need to call this
		//to update the pagesize
		int start = ((DisassembledItem)model.getElementAt(0)).getAddress();
		int end = ((DisassembledItem)model.getElementAt(model.getSize() - 2)).getAddress();
		scroller.setVisibleAmount((end - start) >>> 1);
		scroller.setBlockIncrement((end - start) >>> 1);
	}
	/**
	 * Invoked when the value of the adjustable has changed.
	 */
	public final void adjustmentValueChanged(AdjustmentEvent e)
	{
		if(resizing)
		{
			//scroll bar will have zeroed so reset it
			scroller.setValue(model.getCurrentPos() >>> 1);
			return;
		}

		//the scrollbar has been adjusted
		if(e.getAdjustable().equals(scroller))
		{
			//JScrollBar always gives the TRACK event type
			int pos = e.getValue();
			logger.info("ScrollBar pos changed to " + pos);
			pos = pos + pos;	// times 2

			//handle shift up one item
/*			int curr = model.getCurrentPos();

			if((pos - curr) == 2)
			{
				//find out how big this item is and add it to the current_pos
				DisassembledItem di = (DisassembledItem)model.getElementAt(0);
				curr += di.getSize();
				model.setCurrentPos(curr);
				scroller.setValue(curr >>> 1);
			}
			else
*/
			{
				model.setCurrentPos(pos);
			}

//			if(!e.getValueIsAdjusting())
//				adjustBlockIncrement();
		}
		else
		{
			logger.info("Something was adjusted ?!?");
		}
	}

	/**
	 * Invoked when the mouse wheel is rotated.
	 *
	 * @see java.awt.event.MouseWheelEvent
	 */
	public final void mouseWheelMoved(MouseWheelEvent e)
	{
		int amount = e.getScrollAmount();
		int notches = e.getWheelRotation();
		int items = notches * amount;
		int curr = model.getCurrentPos();

		if(notches < 0)
		{
			//scroll up
			curr += (items << 1);

			if(curr < 0)
			{
				curr = 0;
			}
		}
		else
		{
			DisassembledItem di;

			if(items < model.getSize())
			{
				di = (DisassembledItem)model.getElementAt(items);
			}
			else
			{
				//page down
				di = (DisassembledItem)model.getElementAt(model.getSize() - 1);
			}
			curr = di.getAddress();
		}

		model.setCurrentPos(curr);
		scroller.setValue(curr >>> 1);
	}

	/**
	 * Invoked when a key has been typed.
	 * See the class description for {@link java.awt.event.KeyEvent} for a definition of
	 * a key typed event.
	 */
	public final void keyTyped(KeyEvent e)
	{
		//ignore
	}

	/**
	 * Invoked when a key has been pressed.
	 * See the class description for {@link java.awt.event.KeyEvent} for a definition of
	 * a key pressed event.
	 */
	public final void keyPressed(KeyEvent e)
	{
		//want to capture cursor keys / page up + dn / home + end
		//also capture copy Ctrl+c ?
		int code = e.getKeyCode();
		switch(code)
		{
			case KeyEvent.VK_UP:
			{
				scrollUp();
				break;
			}
			case KeyEvent.VK_DOWN:
			{
				scrollDown();
				break;
			}
			case KeyEvent.VK_PAGE_UP:
			{
				pageUp();
				break;
			}
			case KeyEvent.VK_PAGE_DOWN:
			{
				pageDown();
				break;
			}
			case KeyEvent.VK_HOME:
			{
				gotoHome();
				break;
			}
			case KeyEvent.VK_END:
			{
				gotoEnd();
				break;
			}
		}
	}

	private void scrollUp()
	{
		int idx = list.getSelectedIndex();
		if(idx == 0)
		{
			int curr = model.getCurrentPos();

			curr -= 2;
			if(curr < 0)
				curr = 0;

			model.setCurrentPos(curr);
			scroller.setValue(curr >>> 1);
			adjustBlockIncrement();
		}
	}

	private void scrollDown()
	{
		int idx = list.getSelectedIndex();
		if(idx == model.getSize() - 1)
		{
			DisassembledItem di = (DisassembledItem)model.getElementAt(1);
			int pos = di.getAddress();
			model.setCurrentPos(pos);
			scroller.setValue(pos >>> 1);
			adjustBlockIncrement();
		}
	}

	private void pageUp()
	{
		int curr = model.getCurrentPos();
		int size = model.getSize();

		//best we can do is to jump up 2 * model size bytes
		curr -= (2 * (size - 1));
		if(curr < 0)
			curr = 0;

		model.setCurrentPos(curr);
		scroller.setValue(curr >>> 1);
		adjustBlockIncrement();
	}

	private void pageDown()
	{
		DisassembledItem di = (DisassembledItem)model.getElementAt(model.getSize() - 1);
		int pos = di.getAddress();
		model.setCurrentPos(pos);
		scroller.setValue(pos >>> 1);
		adjustBlockIncrement();
	}

	private void gotoHome()
	{
		model.setCurrentPos(0);
		scroller.setValue(0);
		adjustBlockIncrement();
	}

	private void gotoEnd()
	{
		//approx
		int size = model.getSize();
		size <<= 1;
		int pos = 0x00fffffe - size;
		model.setCurrentPos(pos);
		scroller.setValue(pos >>> 1);
		adjustBlockIncrement();
	}

	/**
	 * Invoked when a key has been released.
	 * See the class description for {@link java.awt.event.KeyEvent} for a definition of
	 * a key released event.
	 */
	public final void keyReleased(KeyEvent e)
	{
		//ignore
	}

	/**
	 * Invoked when the mouse button has been clicked (pressed
	 * and released) on a component.
	 */
	public final void mouseClicked(MouseEvent e)
	{
		if(e.getClickCount() == 2)
		{
			int index = list.locationToIndex(e.getPoint());
			if(index != -1)
			{
				DisassembledItem di = (DisassembledItem)model.getElementAt(index);
				int address = di.getAddress();

				//is address in the breakpoint list ?
				if(debugger.isBreakpointHere(address))
				{
					//remove breakpoint
					debugger.clrBreakpoint(address);
				}
				else
				{
					//set breakpoint
					debugger.setBreakpoint(address);
				}
			}
		}
	}

	/**
	 * Invoked when a mouse button has been pressed on a component.
	 */
	public final void mousePressed(MouseEvent e)
	{
	}

	/**
	 * Invoked when a mouse button has been released on a component.
	 */
	public final void mouseReleased(MouseEvent e)
	{
	}

	/**
	 * Invoked when the mouse enters a component.
	 */
	public final void mouseEntered(MouseEvent e)
	{
	}

	/**
	 * Invoked when the mouse exits a component.
	 */
	public final void mouseExited(MouseEvent e)
	{
	}

	/**
	 * Invoked when an action occurs.
	 */
	public void actionPerformed(ActionEvent e)
	{
		if(e.getActionCommand().equals("Go"))
		{
			//address button clicked
			String addr = addrBox.getText();
			if(addr == null || addr.length() == 0)
				return;

			try
			{
				int val = Integer.parseInt(addr, 16);
				int newVal = val & 0x00fffffe;
				addrBox.setText(TextUtil.toHexNoLeadingZero(newVal));
				model.setCurrentPos(newVal);
				list.setSelectedIndex(0);
				scroller.setValue(newVal >>> 1);
			}
			catch(NumberFormatException ex)
			{
				logger.info("Invalid number specified in addr box");
				JOptionPane.showMessageDialog(this.getParent(), "Please specify a valid address in hexadecimal format", "Invalid Address", JOptionPane.ERROR_MESSAGE);
				addrBox.selectAll();
				addrBox.requestFocus();
			}
		}
	}

	private class DisassemblyRenderer extends JPanel implements ListCellRenderer
	{
		private String textToRender;
		private final int borderSize = 2;
		private final int baseline;
		private final int width;
		private final int height;

		DisassemblyRenderer(int baseline, int height, int width)
		{
			super();
			this.baseline = baseline;
			this.height = height;
			this.width = width;
		}

		public Dimension getPreferredSize()
		{
			return new Dimension(width, height);
		}

		@Override
		public void paint(Graphics g)
		{
			g.setColor(getBackground());
			g.fillRect(0, 0, getWidth(), getHeight());
			g.setColor(getForeground());
			g.drawString(textToRender, borderSize, baseline);
		}

		/**
		 * Return a component that has been configured to display the specified
		 * value. That component's <code>paint</code> method is then called to
		 * "render" the cell.  If it is necessary to compute the dimensions
		 * of a list because the list cells do not have a fixed size, this method
		 * is called to generate a component on which <code>getPreferredSize</code>
		 * can be invoked.
		 *
		 * @param list		 The JList we're painting.
		 * @param value		The value returned by list.getModel().getElementAt(index).
		 * @param index		The cells index.
		 * @param isSelected   True if the specified cell was selected.
		 * @param cellHasFocus True if the specified cell has the focus.
		 * @return A component whose paint() method will render the specified value.
		 * @see javax.swing.JList
		 * @see javax.swing.ListSelectionModel
		 * @see javax.swing.ListModel
		 */
		public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus)
		{
			DisassembledItem item = (DisassembledItem)value;
			boolean bp = debugger.isBreakpointHere(item.getAddress());

			if(isSelected)
			{
				setBackground(list.getSelectionBackground());
				setForeground(list.getSelectionForeground());
			}
			else if(bp)
			{
				setBackground(Color.RED);
				setForeground(Color.WHITE);
			}
			else
			{
				setBackground(list.getBackground());
				setForeground(list.getForeground());
			}
			textToRender = value.toString();
			return this;
		}
	}
}
